#!/usr/bin/perl
#
# File: DAMit.pl
# Author: Colin Mollenhour (mollenho@cs.utk.edu or colin@mollenhour.com)
# http://colin.mollenhour.com
# Version: 3.0
#
# Uses advanced search and replace patterns specified in a DAM (Document for
# API Modification) document to apply API modifications to your source code.
#
# Use --dry-run to see the modifications made without applying them.
# Use --conflict to have VCS style conflict markers added to teh source code
#   wherever changes are made so that you can use your favorite merge tool to merge
#   the DAM changes.
# Use --debug to see a list of compile patterns for the given DAM document
#
# DAM document format specifications:
#
# - Comments are denoted by # and are allowed anywhere in the document
# - Search and Replace are separated by ==> and whitespace
# - Perl substitution modifiers can be specified between == and >. E.g. ==e>
# - The 'g' modifier is assumed unless '-g' is specified
# - $ and / in the search patterns are automatically escaped
# - / in the replacement pattern is automatically escaped
# - Word boundaries (\b) are automatically added to the begin and end of search
#    unless a non-word character exists there
#
# Example DAM document:
#
# header1\.h                          ==>     newheader.h
# rotate_object(\d)d                  ==>     RotateObject$1D
# new MyClass                         ==>     MyClass::factory
# (MyClass1|MyClass1)->               ==>     MyNamespace::$1->
# foo_(\w+)_bar                       ==>     Foo$1Bar
# ([A-Z][a-z]+Model)::([A-Z_]+)       ==e>    lc("'$2'")     # convert constants to strings
# \s{4}                               ==e-g>  ""             # non-global match

$USAGE =	
"Usage: DAMit.pl [--conflict] [--dry-run] dam.txt file [file] ...
  --conflict   write VCS style conflicts markers, must be removed after reviewing
  --dry-run    output the log, but do not apply changes

Examples:
DAM all files in current directory:
   \$ DAMit.pl --conflict dam.txt *.*
DAM all .h, .c, and .cpp files in current directory (recursive):
   \$ DAMit.pl dam.txt `find -name \\*.cpp` `find -name \\*.\\[hc\\]`
";


# check arguments for options
$TESTING = 0;
$WRITECONFLICTS = 0;
$DEBUG = 0;
foreach $arg (@ARGV){
	if(    $arg eq "--dry-run"){  $TESTING = 1; }
	elsif( $arg eq "--conflict"){ $WRITECONFLICTS = 1; }
	elsif( $arg eq "--debug"){    $DEBUG = 1; }
	elsif( $arg =~ /^-/ ){ die("Invlaid argument $arg\n\n$USAGE"); }
	else{ last; }
}
if($TESTING){ shift(@ARGV); }
if($WRITECONFLICTS){ shift(@ARGV); }
if($DEBUG){ shift(@ARGV); }
die($USAGE) unless @ARGV > 1;


# build the DAM patterns list
$damfile = shift(@ARGV);
open(DAM, $damfile) || die( "Unable to open $damfile : $!\n" );
@patterns = ();
while( <DAM> ){
	next if /^#/;	#skip comments
	chop;
	if (($search, $mods, $replace) = ($_ =~ /^(.*?)\s+==([msixpogce-]*)>\s+(.*?)(?:\s+#.*)?$/)){

		# Auto-escape and auto-word boundary for search
		$search =~ s#([\$/])#\\$1#g;
		$search =~ s/^(\w)/\\b$1/;
		$search =~ s/(\w)$/$1\\b/;

        # Remove global match by -g, otherwise g is default. o is also default
		if (($mods =~ /-g/)){ $mods =~ s/-g.*//; }
		else{ $mods .= 'g'; }

		# Auto-escape / in replacement
		$replace =~ s#(/)#\\$1#g;

		push( @patterns, "s/$search/$replace/$mods" );
	}
}
close DAM;

print "Patterns:\n".join("\n",@patterns)."\n\n" if $DEBUG;

# process each file
foreach $arg (@ARGV){
	open FILE, "+<$arg" or die "Cannot open $arg: $!\n";
	flock FILE, 2;		# lock the file for rw
	@data = <FILE>;		# buffer all the data

	print "For file: $arg\n";
	$total = 0; # track total number of replacements
	$line = 1;  # line number tracker

	# Variables for tracking conflicts
	@changes = (); $conflict = 0;
	@newdata = ();

	# scan each line
	foreach (@data){
		$before = $_;
		$replace = 0;
		foreach $pattern ( @patterns ){
			eval "\$replace += (\$_ =~ ${pattern}o)";
		}
		if($WRITECONFLICTS){
			if($replace){                                        # changes made
				push( @newdata, "<<<<<<< mine\n" ) if !$conflict; # start new conflict
				$conflict = 1;
				push( @changes, $_ );                              # store changes for later
			}
			elsif( $conflict ){                                # no change, write and end conflict
				push( @newdata, "=======\n" );
				push( @newdata, @changes );
				push( @newdata, ">>>>>>> $damfile\n" );
				@changes = ();
				$conflict = 0;
			}
			push( @newdata, $before );
		}
		$total += $replace;
		print "$line: -$before$line: +$_" if $replace;
		$line++;
	}

	# print status and write file (if necessary)
	if( $total == 0 ){
		print "No replacements made in $arg\n\n";
	}else{
		if( !$TESTING ){   #overwrite file with new contents
			seek FILE, 0, 0;
			truncate FILE, 0;
			if($WRITECONFLICTS){
				print FILE @newdata;
			}else{
				print FILE @data;
			}
		}
		print "Made $total total replacements in $arg\n\n";
	}
	close FILE;
}

print "DAM'ed ".scalar(@ARGV)." files.\n";

